package ge.tsu.java.project.client;

import ge.tsu.java.project.commons.BinaryProtocol;
import ge.tsu.java.project.commons.Contact;
import ge.tsu.java.project.commons.Logger;
import ge.tsu.java.project.commons.Message;
import ge.tsu.java.project.commons.Message.Type;
import ge.tsu.java.project.commons.Protocol;

import java.io.IOException;
import java.net.Socket;
import java.util.List;

public class ChatClient {

	private static Logger LOGGER = Logger.getInstance(ChatClient.class);

	private ClientConfig config;
	private Socket socket;
	private Protocol protocol;
	private MessageListenerThread eventListener = new MessageListenerThread();
	private volatile boolean connected;
	private volatile boolean inputBlocked;
	private boolean loggedin;
	private String userId;

	public ChatClient(ClientConfig config, ChatEventListener l) {
		this.config = config;
		eventListener.listener = l;
	}

	public synchronized void connect() throws IOException {
		if (!connected) {
			socket = new Socket(config.getAddress(), config.getPort());
			protocol = new BinaryProtocol(socket.getInputStream(), socket.getOutputStream());
			connected = true;
		} else {
			LOGGER.log("Client already connected, ignoring...");
		}
	}

	public synchronized boolean login(String userId, String password) throws IOException {
		checkConnected();
		if (loggedin) {
			return true;
		}
		protocol.writeMessage(Message.newMessage(Type.AUTHENTICATE));
		protocol.writeString(userId);
		protocol.writeString(password);
		loggedin = protocol.readBoolean();
		if (loggedin) {
			this.userId = userId;
			eventListener.start();
		}
		return loggedin;
	}

	public synchronized boolean register(String userId, String password) throws IOException {
		checkConnected();
		if (loggedin) {
			throw new RuntimeException("Cannot register, already logged in");
		}
		protocol.writeMessage(Message.newMessage(Type.REGISTER));
		protocol.writeString(userId);
		protocol.writeString(password);
		return protocol.readBoolean();
	}

	public synchronized void sendChatMessage(String message, String to) throws IOException {
		checkConnected();
		checkLoggedIn();
		if (userId.equals(to)) {
			throw new RuntimeException("Cannot send message to self");
		}
		protocol.writeMessage(Message.newMessage(Type.CHAT_MESSAGE));
		protocol.writeString(message);
		protocol.writeString(to);
	}

	@SuppressWarnings("unchecked")
	public synchronized List<Contact> getContacts() throws IOException {
		checkConnected();
		checkLoggedIn();
		inputBlocked = true;
		protocol.writeMessage(Message.newMessage(Type.GET_CONTACT_LIST));
		protocol.writeString(userId);
		List<Contact> contacts = (List<Contact>) protocol.readSerializable();
		inputBlocked = false;
		return contacts;
	}

	public synchronized void disconnect() {
		if (connected) {
			try {
				protocol.writeMessage(Message.newMessage(Type.EOF));
			} catch (IOException e) {
				// ignore
			}
			close();
		}
	}
	
	private void close() {
		connected = false;
		loggedin = false;
		try {
			socket.close();
		} catch (IOException e) {
			
		}
	}

	private void checkConnected() {
		if (!connected) {
			throw new RuntimeException("Not connected");
		}
	}

	private void checkLoggedIn() {
		if (!loggedin) {
			throw new RuntimeException("Not logged in");
		}
	}

	private class MessageListenerThread extends Thread {

		private ChatEventListener listener;

		@Override
		public void run() {
			while (connected) {
				try {
					waitForMessageAvailability(); 
					// XXX there's small room between 
					// this line and synchronized block
					// and this is buggy
					synchronized (ChatClient.this) {
						if (!connected) break; // re-check connection status 
						Message msg = protocol.readMessage();
						Type msgType = msg.getType(); 
						if (msgType == Type.CHAT_MESSAGE) {
							String message = protocol.readString();
							Contact from = (Contact) protocol.readSerializable();
							if (listener != null && connected) {
								listener.onMessage(message, from);
							}
						} else if (msgType == Type.CONTACT_STATUS_CHANGED) {
							// გადავიფიქრე :D
							Contact contact = (Contact) protocol.readSerializable();
							if (listener != null && connected) {
								listener.onContactStatusChanged(contact);
							}
						} else {
							// not supported other message types
							LOGGER.err("Bad server, expected MessageType: " +
									"CHAT_MESSAGE or CONTACT_STATUS_CHANGED, " +
									"instead got: " + msgType.toString());
						}
					}
				} catch (IOException e) {
					if (connected) {
						ChatClient.this.close();
						LOGGER.err("Error while listening for messages", e);
					}
				}
			}
		}

		private void waitForMessageAvailability() throws IOException {
			while (inputBlocked || (connected && protocol.available() == 0)) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) { }
			}
		}
	}

}
